A client for the ‘WebDriver’ ‘API’: driving a web browser. It works with any ‘WebDriver’ implementation, but it was only tested with ‘PhantomJS’.
Selenium é uma ferramenta de código aberto popular para automação de navegadores web, e utiliza a API WebDriver, uma interface padrão para controle de navegadores, que também é acessível em R por meio do pacote “webdriver”, possibilitando a automação de navegadores para testes e raspagem de dados na web.
Código
library(webdriver)## Define Main URLurl <-"https://sigaa.unb.br/sigaa/public/componentes/busca_componentes.jsf"## Init Session and start scraping by query typeinit_session <-function(type_of_query,url){# Init Library, Session and Navigate to URL pjs <<-run_phantomjs() s <<- Session$new(port = pjs$port) s$go(url)# First Search Boxes## Select "Graduação" search_nivel <- s$findElement(css ="option[value='G']") search_nivel$click()## Select "Disciplinas" search_tipo <- s$findElement(xpath ='//select[@id="form:tipo"]//option[@value="2"]') search_tipo$click()# Search only in the following 'unidades' according to `type_of_query`## - DEPARTAMENTO## - DEPTO## - FACULDADE## - INSTITUTO switch(type_of_query,## Select "Departamentos"departamentos = { query <- s$findElements(xpath ='//select[@id="form:unidades"]//option[starts-with(text(),"DEPARTAMENTO") or starts-with(text(),"DEPTO")]')},## Select "Faculdades"faculdades = { query <- s$findElements(xpath ='//select[@id="form:unidades"]//option[starts-with(text(),"FACULDADE")]')},## Select "Institutos"institutos = { query <- s$findElements(xpath ='//select[@id="form:unidades"]//option[starts-with(text(),"INSTITUTO") ]')} )return(query)}## Functions iterate_unit_list <-function(unit_list){if(!is.list(unit_list)) {message("Argument is not a list")return() }message( glue::glue("Process started at {Sys.time()} ")) total_size <-length(unit_list)# DEFINE SCRAPING RANGE HERE ## - uncomment the next line, change to desired range# - comment the other for, or use it uncommented to get ALL data at once.#for(i inc(43:44)){# Submit full search criteriabrowser()message( glue::glue("Scanning ",unit_list[[i]]$getText()," [{i}/{total_size}]")) unit_list[[i]]$click() submit_search <- s$findElement(xpath ='//input[@id="form:btnBuscarComponentes"]') submit_search$click()## Create the index and start scanning if not empty. details_list <- s$findElements(xpath ="//a[contains(@title, 'Detalhes')]" )if(length(details_list) ==0){message("No subjects found: Continuing with next unit... ")# Rebuild Index unit_list <- s$findElements(xpath ='//select[@id="form:unidades"]//option[starts-with(text(),"DEPARTAMENTO") or starts-with(text(),"DEPTO")]')next } content <-get_subject_content(details_list)# Write this Unit CSV to persist dataexport_content(content)# Rebuild Index unit_list <- s$findElements(xpath ='//select[@id="form:unidades"]//option[starts-with(text(),"DEPARTAMENTO") or starts-with(text(),"DEPTO")]') }message(glue::glue("Process Completed at {Sys.time()}"))return()}get_subject_content <-function(subject_list){if(!is.list(subject_list)) {message("Argument is not a list")return() } total_size <-length(subject_list) tables <- tibble::tibble()for(i in1:length(subject_list)){# Go to detail page subject_list[[i]]$click()# Grab its table in a nice format## The '25' is a khabalistic-pseudo-safe number that I suppose will be safe enough## to get any number of other non-standard fields and still get the last one with the "Ementa" table <- s$findElements(xpath ="//table[@class='visualizacao']/tbody/tr[position() <= 25]/td[not(table)]/..") details <- table |> purrr::reduce(\(acc,e){ paste0(acc, e$executeScript("return arguments[0].outerHTML")) }, .init ="<table>") |>append("</table>") |>paste0(collapse="") |> rvest::read_html() |> rvest::html_table() |> purrr::pluck(1) |> dplyr::filter( stringr::str_starts(X1,"Tipo do Componente") | stringr::str_starts(X1,"Modalidade") | stringr::str_starts(X1,"Unidade") | stringr::str_starts(X1,"Código") | stringr::str_starts(X1,"Nome") | stringr::str_starts(X1,"Pré-Requisitos") | stringr::str_starts(X1,"Co-Requisitos") | stringr::str_starts(X1,"Equivalências") | stringr::str_starts(X1,"Ementa") ) |> tidyr::pivot_wider(names_from=X1, values_from=X2, values_fill=NA)message("Details ✅")# Grab Workload find_workload <- \(path){s$findElement(xpath = path)$getText()} possibly_find_workload <- purrr::possibly(find_workload, otherwise ="Subtotal de Carga Horária de Aula - Presencial \n0h") workload <-possibly_find_workload("//table[@class='visualizacao']//td[b[contains(text(),'Subtotal de Carga Horária de Aula - Presencial')]]/following-sibling::td/..") workload_df <- workload |> stringr::str_split("\n", simplify =TRUE) |> (\(.workload){ tibble::tibble( "{.workload[1]}":= .workload[2])})()# Concatenate Details and Workload details_df <- tibble::add_column(details, workload_df)message("Workload ✅")##### Go back to index page #### back <- s$findElement(xpath ="//a[text()=' << Voltar ']") back$click()# Try to get detailed program program_list <- s$findElements(xpath ="//a[contains(@title, 'Programa')]" )tryCatch( { program_list[[i]]$click() program <- s$findElements(css =".itemPrograma") }, # If fails, close error modal and rebuild indexerror =function(e) { message("No program available. Going Back...") error <- s$findElement(css ="#fechar-painel-erros > a") error$click() },finally = program_list <- s$findElements(xpath ="//a[contains(@title, 'Programa')]" ) )# If succeeds, process program data and go back to main page.if(length(program) >0) { program_df <- tibble::tibble(Objetivos = program[[1]]$getText(), "Conteúdo"= program[[2]]$getText()) s$goBack() } else { program_df <- tibble::tibble(Objetivos =NA, "Conteúdo"=NA) }message("Program ✅")# Recreate the index subject_list <- s$findElements(xpath ="//a[contains(@title, 'Detalhes')]" ) program_list <- s$findElements(xpath ="//a[contains(@title, 'Programa')]" )# Concatenate to final structure full_table_df <- tibble::add_column(details_df, program_df) tables <- tibble::add_row(full_table_df, tables) message(glue::glue("Subject {i} of {total_size} scraped!")) }return(tables)}# Export final data frame to CSVexport_content <-function(content) {print(content) janitor::clean_names(content) |> (\(df){ clean_name <- stringr::str_remove_all(df$unidade_responsavel[1], "[:digit:]") |> janitor::make_clean_names() print(clean_name)write.csv(df, file =paste0(clean_name,".csv"), row.names =FALSE) } )()}
Todas as diciplinas de Instututos, Faculdades e departamentos da UnB (>8000).
Escolhemos Analisar: Instituto de Exatas, Faculdade de Tecnologia, Física, Química. Economia.
ChatGPT
Reinforcement learning
What is reinforcement learning?
Reinforcement learning é uma área de aprendizado de máquina que se preocupa com a forma como os agentes do software devem agir em um ambiente para maximizar a noção de recompensa cumuativa
Trata-se da introdução de um viés humano no modelo de linguagem
Processo de aprendizagem do GPT
O GPT usa a técnica de Reinforcement Learning from Human Feedback (RLHF) para minizar saídas perigosas, falsas ou viesadas do modelo. É um processo em três fases:
Modelo Supervised Fine-Tuning (SFT) [12-15k data points]
Reward model (RM) [30-40k prompts]
Fine-tuning do modelo SFT via Proximal Policy Optimization (PPO)
onde \(r_t(\theta) = \frac{\pi_\theta}{\pi_{\theta_{old}}}\), \(\pi\) se refere a uma política, \(A^t\) é um advantage estimator e \(\epsilon\) é um hiperparâmetro pequeno
A função clip garante que a razão entre as políticas não desvie significativamente do intervalo \([1 - \epsilon, 1 + \epsilon]\)
Uma política \(\pi(s)\) compreende as ações sugeridas que o agente deve realizar para cada estado possível \(s \in S\), seguindo um Markov decision process.
Proximal Policy Optimization (PPO)
O PPO é um algoritmo que otimiza a função de perda de uma política de aprendizagem por reforço usado na OpenAI desde 2017
O PPO busca um equilíbrio entre a facilidade de implementação, a complexidade da amostragem e a facilidade de ajuste
Tenta calcular uma atualização em cada etapa que minimize a função de custo e, ao mesmo tempo, garantindo que o desvio da política anterior seja relativamente pequeno.
Como conectou com a API
Código
# importsfrom openai import OpenAIimport pandas as pdimport timefrom tqdm import tqdmfrom multiprocessing import Pool, cpu_countimport osfrom wakepy import keep# setupapi_key =open('api_key.txt').read().strip()client = OpenAI(api_key=api_key)obj_len =150prompt =f"""Gostaria de usar o texto base para fazer uma descrição mais inteligível, direta, e bem explicada de no máximo {obj_len} palavras que vou chamar de Ementa Padronizada. Para que você tenha um contexto, o texto que estou te fornecendo corresponde a uma disciplina de nível de graduação em uma universidade federal no brasil. Eu gostaria dessa ementa em um texto corrido e não em tópicos. Também gostaria que excluísse descrição de processos avaliativos usados e que se atenha a descrição do conteúdo que será ensinado de modo geral. Não é necessário detalhar como será dividido o conteúdo como módulos/unidade ou equivalentes, apenas a descrição do conteúdo abordado de forma geral. Peço também que não faça referencias ao fato de ser uma ementa padronizada, e retorne apenas o texto que voce produzir e nada mais.""".strip().replace('\n', '')def process_row(row):try: response = client.chat.completions.create( model="gpt-4", messages=[ {"role": "system", "content": prompt}, {"role": "user", "content": row['conteudo_completo']}, ] ).choices[0].message.contentreturn responseexceptExceptionas e:print(f"Error: {e}")returnNonedef init_worker():print(f"Initializing worker process {os.getpid()}")def save_progress(ementas, filename='ementas.csv'): ementas.to_csv(filename, index=False)# print("Progress saved to disk.")if__name__=="__main__": ementas = pd.read_csv('ementas.csv')if'conteudo_padronizado_gpt4'notin ementas.columns: ementas['conteudo_padronizado_gpt4'] = pd.Series(index=ementas.index, dtype=str) departments = ['INSTITUTO DE CIÊNCIAS EXATAS','DEPTO ENGENHARIA FLORESTAL','DEPTO ENGENHARIA DE PRODUCAO','DEPTO ESTATÍSTICA','DEPARTAMENTO DE MATEMÁTICA','DEPTO ECONOMIA','DEPTO CIÊNCIAS DA COMPUTAÇÃO','DEPTO ENGENHARIA CIVIL E AMBIENTAL','DEPTO ENGENHARIA ELETRICA','INSTITUTO DE QUÍMICA','INSTITUTO DE FÍSICA','FACULDADE DE TECNOLOGIA' ] ementas = ementas[ementas['unidade_responsavel'].isin(departments)] pool_size = cpu_count() pool = Pool(pool_size, initializer=init_worker) missing_rows = ementas[ementas['conteudo_padronizado_gpt4'].isna()]with keep.running():for i, row in tqdm(missing_rows.iterrows(), total=len(missing_rows)): result = pool.apply_async(process_row, args=(row,)) ementas.loc[i, 'conteudo_padronizado_gpt4'] = result.get()if i %32==0: save_progress(ementas) save_progress(ementas) pool.close() pool.join()
O que usamos dos dados
Nome + ementa + descrição + conteúdo - > Minúscula, sem acentos/carac. especiais
Promtp usado
Gostaria de usar o texto base para fazer uma descrição mais inteligível, direta, e bem explicada de no máximo 150 palavras que vou chamar de Ementa Padronizada. Para que você tenha um contexto, o texto que estou te fornecendo corresponde a uma disciplina de nível de graduação em uma universidade federal no brasil. Eu gostaria dessa ementa em um texto corrido e não em tópicos. Também gostaria que excluísse descrição de processos avaliativos usados e que se atenha a descrição do conteúdo que será ensinado de modo geral. Não é necessário detalhar como será dividido o conteúdo como módulos/unidade ou equivalentes, apenas a descrição do conteúdo abordado de forma geral. Peço também que não faça referencias ao fato de ser uma ementa padronizada, e retorne apenas o texto para que seja fácil copiá-lo.
VIGILANCIA SANITARIA, DEONTOLOGIA E LEGISLACAO FARMACEUTICA
Esta disciplina de nível de graduação em farmácia foca na vigilância sanitária, deontologia e legislação farmacêutica. O curso visa introduzir o estudante à legislação atual que rege a produção, comercialização, prescrição, informação e dispensação de medicamentos. Também é abordada a legislação do sistema de saúde e da vigilância sanitária, além de se destacar os aspectos éticos da profissão farmacêutica.
O conteúdo inclui uma exploração da história da profissão farmacêutica, a evolução do conceito de ética profissional, e as regulamentações que influenciam a prática farmacêutica. Os alunos são incentivados a desenvolver uma reflexão crítica sobre os dilemas éticos da profissão. O curso também proporciona um entendimento sobre vigilância sanitária, incluindo seu papel no sistema de saúde, o processo de registro de medicamentos, e as práticas relacionadas à informação e propaganda de medicamentos.
Além disso, o curso abrange temas como práticas de produção e inspeção farmacêutica, a defesa do consumidor em relação a medicamentos, e o controle de qualidade laboratorial dentro do contexto da vigilância sanitária. O objetivo é preparar os alunos para compreender e aplicar as leis e regulamentos do campo farmacêutico, fomentando uma prática ética e responsável.
Embeding
O que é Embeding
O “embedding” é uma técnica em aprendizado de máquina que transforma dados complexos e de alta dimensão, como textos ou imagens, em vetores de baixa dimensão, preservando as relações semânticas e contextuais.
O k-Means é um algoritmo de agrupamento que divide dados em ( k ) grupos, minimizando a variação interna e ajustando os centróides de cada grupo iterativamente até a convergência.
t-SNE
O t-SNE é uma técnica de abordagem não-linear de redução de dimensionalidade, focado na preservação sas semelhanças locais, ideal para visualizar agrupamentos em duas ou três dimensões.
Clusterizacao
Resultados fodásticos
import numpy as npimport pandas as pdimport plotly as pltfrom ast import literal_evalfrom sklearn.cluster import KMeansfrom sklearn.manifold import TSNEdf = pd.read_csv('ementas.csv')df["embeddings_ada"] = df.embeddings_ada.apply(literal_eval).apply(np.array)matrix = np.vstack(df.embeddings_ada.values)matrix.shapen_clusters =len(df['unidade_responsavel'].unique())kmeans = KMeans(n_clusters=n_clusters, init="k-means++", random_state=42)kmeans.fit(matrix)labels = kmeans.labels_df["Cluster"] = labelscluster_names = []for i inrange(n_clusters):print(f'Cluster {i}') names =' '.join(df[df['Cluster'] == i]['nome'].to_list())print(len(names)) cluster_names.append(names)print(names) generated_names = ['Ciências Físicas Avançadas','Economia e Política Econômica','Estatística e Métodos Quantitativos','Ciência da Computação e Sistemas','Engenharia Civil e Infraestrutura','Matemática Avançada e Aplicada','Gestão e Projeto Interdisciplinar','Engenharia de Redes e Telecomunicações','Gestão Ambiental e Sustentabilidade','Química Teórica e Aplicada','Estágio Supervisionado e Regência','Engenharia Elétrica e Eletrônica' ]fig, ax = plt.subplots(figsize=(15, 5)) tsne = TSNE(n_components=2, perplexity=15, random_state=42, init="random", learning_rate=200) vis_dims2 = tsne.fit_transform(matrix) x = [x for x, y in vis_dims2] y = [y for x, y in vis_dims2] deps = df['unidade_responsavel'].unique().tolist()for category, color inenumerate([plt.get_cmap("tab20")(i) for i inrange(n_clusters)]): xs = np.array(x)[df.unidade_responsavel == deps[category]] ys = np.array(y)[df.unidade_responsavel == deps[category]] ax.scatter(xs, ys, color=color, alpha=0.2) avg_x = xs.mean() avg_y = ys.mean() ax.annotate( deps[category], (avg_x, avg_y), horizontalalignment='center', verticalalignment='center', size=10, weight='bold', color=color, alpha=1 ) ax.set_title("Visualização do Embedding dos Departamentos usando t-SNE") plt.show()
t-SNE Departamentos
t-SNE Departamentos
t-SNE Estatística
t-SNE Economia
Protótipo
import pandas as pdimport numpy as npfrom ast import literal_evalfrom sklearn.metrics.pairwise import cosine_similarityfrom openai import OpenAIimport gradio as grapi_key =open('api_key.txt').read().strip()client = OpenAI(api_key=api_key)df = pd.read_csv('ementas.csv')df["embeddings_ada"] = df.embeddings_ada.apply(literal_eval).apply(np.array)def get_embedding(text, model="text-embedding-ada-002"): text = text.replace("\n", " ")return client.embeddings.create(input=[text], model=model).data[0].embeddingdef get_recommendations(text): text_embedding = np.array(get_embedding(text)).reshape(1, -1) similarity = df['embeddings_ada'].apply(lambda x: cosine_similarity(x.reshape(1, -1), text_embedding.reshape(1, -1)).item()) similarity = similarity.sort_values(ascending=False).head(10) similarity = df.iloc[similarity.index].drop_duplicates(subset=['nome']).drop(columns=['conteudo_completo', 'embeddings_ada']) similarity.index =range(1, len(similarity)+1)return similaritydef recommend(text):try: recommendations = get_recommendations(text)# Convert the DataFrame to HTML for renderingreturn recommendations.to_html(escape=False)exceptExceptionas e:returnstr(e)with gr.Blocks() as demo: gr.Markdown("## Course Recommendation System") gr.Markdown("Entre suas preferências de acadêmicas e nós te recomendaremos os 10 cursos mais similares da área de exatas + engenharias.")with gr.Row(): text_input = gr.Textbox(lines=2, placeholder="Enter Description Here", label="Descreva seus interesses acadêmicos")with gr.Row(): submit_button = gr.Button("Submit") output = gr.HTML() submit_button.click(recommend, inputs=text_input, outputs=output)demo.launch()
Conclusões e Recomendações futuras
É difícil conseguir os dados da UnB.
Dá pra fazer WebScraping no R
A API da OpenAI é ótima (e cara!)
Da pra usar outros modelos além do ChatGPT
Foi possível criar clusters relativamente coesos.
Analisar quais as disciplinas “distoantes”.
Dashboard para a consulta por parte dos alunos.
Análise mais formal de sobreposição entre cursos ou falta de coesão no currículo.